home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Tech Arsenal 1
/
Tech Arsenal (Arsenal Computer).ISO
/
tek-01
/
cljul90.zip
/
SPAWN.C
< prev
Wrap
Text File
|
1990-06-19
|
31KB
|
646 lines
UNIX Process Control
by Lyle Frost
Process control is an important element of programming on multitasking
systems. It includes operations such as process creation, termination, and
synchronization. UNIX is a multiuser as well as multitasking operating system,
and this topic is particularly important in the development of multiuser
applications.
UNIX operating system services such as process control are accessed using
system calls. System calls are functions within the operating system kernel
(the heart of the system) which have been made accessible to the application
programmer. An understanding of how to use the relevant system calls is
critical to being able to control UNIX processes.
UNIX Processes
A process is an instance of execution of a program. Processes should not
be confused with programs, which are the files executed by processes. On a
multitasking system, more than one process may be executing the same program
concurrently, and each process may transform itself to execute a different
program.
Every process on a UNIX system has a unique identifier called the process
ID (PID). A PID is a positive integer assigned by the operating system to each
process when it is created. A process may obtain its PID using the getpid
system call, int getpid(), which returns the PID of the calling process. Since
the PID is used to uniquely identify a process, it may not be changed, though
it may be reused when the process no longer exists.
Each new process on a UNIX system is created by a previously existing
process, producing a parent-child relationship. A process may obtain the PID
of its parent using the getppid system call, int getppid(), which returns the
PID of the parent of the calling process. A process can not change its parent
PID.
As a result of the parent-child relationship, processes possess a
tree-structured hierarchy. This is referred to as the process tree. On every
UNIX system, the root of the process tree is a special process called the
swapper. The swapper is created with a PID of 0 when the system is booted. It
manages the allocation of memory for processes and influences the allocation of
the CPU. The first child created by the swapper (by executing the file
/etc/init) is the process dispatcher, which is assigned a PID of 1. All
processes initiated by users are descendants of the process dispatcher. All
other children of the swapper are special system processes which execute code
entirely within the operating system kernel. Both the swapper and the process
dispatcher exist for the lifetime of the system.
UNIX keeps track of processes in an internal data structure called the
process table, which has an single entry for every process on the system. A
listing of the process table can be obtained using the ps command. Combining
the -e (every process) and -f (full listing) options will cause ps to display
all relevant information for every process in the table.
Process Creation
New processes are created using the fork system call, int fork(), which
creates a new process that is the child of the calling process. The child
process is an almost exact duplicate of the parent, and although it did not
exist prior to the call to fork, it "resumes" execution of the same program at
the same place as does its parent (i.e., at the return of the call to fork).
The value returned by fork is the simplest way to distinguish the parent from
the child; the PID of the child process is returned to the parent, while a
value of 0 is returned to the child. On error, a value of -1 is returned to
the parent and no new process is created. The following C code fragment
outlines the use of fork.
int pid;
pid = fork();
switch (pid) {
case -1: /* error */
/* error handling */
break;
case 0: /* child process */
/* code executed by child */
break;
default: /* parent process */
/* code executed by parent (pid == PID of child) */
break;
};
Included in the duplication of the parent process is its file descriptor
table. A file descriptor is an integer value used by a process to reference an
open file. This integer value is used by system calls as an index into the
file descriptor table of the process which has the file open. Since the file
descriptor table is duplicated, all files open in the parent at the time fork
is called will be open in the child.
A second data structure called the system file table is also used in the
control of open files. The file pointer (current file position) and access
mode (e.g., read-only) are both stored in this table. When a file is opened,
an unused entry in the system file table is allocated. An unused entry in the
file descriptor table is then allocated and linked to that system file table
entry. While each process has its own file descriptor table, the system file
table is global and is not duplicated when a new process is created. Hence,
inherited file descriptors, which are otherwise independent, share the same
system file table entry and hence the same file pointer and access mode. Note
that this is the same relationship produced by the dup system call between file
descriptors of the same process. Attention should be paid to the possibility
of processes interfering with each other by modifying the file pointer or
access mode linked to an inherited file descriptor, since this can be an
elusive source of problems if allowed to occur inadvertently.
A second caveat exists when file access is buffered, as is the case when
using the C stdio library. When data is written to a file using stdio, it is
stored in a buffer until either the buffer is full, an explicit call is made to
flush the buffer, or the file is closed, at which time write (the system call
to write data to a file) is called to actually write the buffer contents to the
file. But since the buffers are part of the process, they are duplicated when
the process is duplicated. This can produce unexpected results if there is
buffered data waiting to be written when fork is called. Examining the program
in Figure 1, fputs is called by the original process to write a single line of
text to a file. After the call to fork both the child and the parent close the
file, causing the buffer contents of each process to be flushed. As a result,
the data is written to the file twice.
Process Transformation
Processes are generally created with the intention of running a new
program. This is done with the exec system call. exec transforms a process by
invoking a new program to replace the one from which the call to exec was
executed. The new program is executed from its beginning. There can be no
return from a successful call to exec because the code containing the call has
been replaced with that of the new program. A call to exec usually, but not
necessarily, closely follows a call to fork.
exec is actually a generic name for a group of six system calls. The
archetypal exec function is execve, which is called with the following
arguments:
int execve(char *path, char *argv[], char *envp[]);
path is the path name (i.e., a file name with a path prefix giving its
location) of the program to execute. argv (argument vector) is an array of
strings which are the arguments to pass to the new program. envp (environment
pointer) is an array of strings of the form "name=value" (e.g., "TERM=VT100")
defining the environment for the new program. The environment normally
contains such information as the user's home directory and the terminal type.
Both argv and envp must be terminated with the NULL pointer.
The five other forms of the exec system call are basically the same as
execve, but with slightly reorganized parameters. The complete list is given
below.
int execl(path, arg0, arg1, ..., argn, NULL)
int execle(path, arg0, arg1, ..., argn, NULL, envp)
int execlp(file, arg0, arg1, ..., argn, NULL)
int execv(path, argv)
int execve(path, argv, envp)
int execvp(file, argv) char *path, *file, *argn, *argv[], *envp[];
Notice first that these functions divide into two groups which differ only
by replacing the argument vector argv by an expanded argument list arg0, arg1,
..., argn, NULL. The functions are named accordingly using v or l to indicate
a vector or a list. Secondly, the functions ending with a p will take the file
name while the others require the path name. The former search the path
defined in the environment of the calling process (e.g., "PATH=:/bin:/usr/bin")
to find the file. Lastly, the functions ending in e allow a new environment to
be passed. The others copy the current environment to the new program.
Whether vector or list, all six exec functions require that the program
name and its arguments be passed as individual tokens. But when this data is
obtained from user input or a file, it is normally a single string and so must
be tokenized into either a vector or a list. Figure 2 shows the source code
for a new exec function, execs, that accepts commands in string form; the code
for the corresponding environment and path functions execse and execsp is
similar.
int execs(char *cmdlin);
int execse(char *cmdlin, char *envp[]);
int execsp(char *cmdlin);
The execs functions tokenize the string cmdlin to create an argument vector
which is then used to call one of the execv functions. The tokenizing is done
with a function called cmdtok (the source for this is not shown). cmdtok is
similar to the ANSI C function strtok, but treats contiguous white space
between tokens as a single space and recognizes the metacharacters \, ', ", and
#. The function of each of these metacharacters is the same as for the shell
(see section 3.2 of KERN84).
An alternative to the execs functions is to invoke the shell with one of
the provided exec functions and let it do the tokenization. The string
containing the command line to be executed is passed as an argument to the
shell as shown below.
execl("/bin/sh", "sh", "-c", cmdlin, NULL);
-c instructs sh to execute the command line in the next argument (cmdlin)
and then terminate; the ANSI C library function system executes a command line
in this way. sh expands any metacharacters in cmdlin and constructs an
argument vector. The new shell calls fork to create a new process which calls
exec with the argument vector. The advantage to this method is that the full
functionality of the shell is available in processing the command line (of
course, additional metacharacters can always be implemented in execs if
needed). One drawback is that an extra process has been created. Also, the
original process knows the PID of the new shell, but not of the process
actually executing the command.
Files remain open across a call to exec. This is convenient for setting
up a program's standard input, output, and error. Any unnecessary files,
however, should be closed before executing a new program. The fcntl system
call can be used to set a file desciptor's close-on-exec flag, causing it to be
closed automatically when exec is called.
A C program may access its argument list through the second parameter of
the function main; main is the entry point of every C program.
int main(int argc, char *argv[]);
argc is set to the number of elements (excluding the terminating NULL) of
argv. By convention, argv[0] is the program name (i.e., the file argument in
execlp and execvp). This is not enforced by exec, but is often required by the
program being executed.
The environment may be accessed through the global pointer
environ: extern char **environ. Most UNIX C implementations allow main a third
parameter char *envp[] for the environment passed to exec, but since this
deviates from the ANSI C standard it is more portable to access the environment
using environ. Also, environ is the actual environment pointer, while envp
simply holds a copy of the environment pointer at the time the program began
execution. When the environment is modified using the library function putenv,
the location of the environment may be changed, in which case the address
stored in envp will no longer be valid.
Process Termination
A process terminates using the exit system call, void exit(int status).
status is a value to be reported back to the parent of the terminating process.
While any interpretation may be placed on the value of status (as long as the
parent and child are consistent), convention dictates that a value of 0 be used
to indicate a successful completion and non-zero values for failure; the
shell's if statement is designed for programs using this convention.
Before terminating the calling process, exit automatically calls fclose
for all open streams. This is important because buffered data would be lost
otherwise. close is called for any open file descriptors not associated with a
stdio stream. If a process terminates while it still has children, the parent
PID of each of the children is changed to 1. Or in other words, orphan
processes are adopted by the process dispatcher. As the final step, exit sends
a "death of child" signal (SIGCLD) to its parent.
Even after a process has terminated, it still has its entry in the process
table. A process that has terminated but has not yet been removed from the
process table is called a zombie. Zombies can be identified from the ps command
listing by <defunct> (or something similar) in the COMMAND column. A process
can only be removed from the process table by its parent. The mechanism for
this will be described in the next section.
Unlike most system calls, exit is included in the ANSI C standard library,
and the macros EXIT_SUCCESS and EXIT_FAILURE are defined in <stdlib.h> for use
as the argument to exit when only a simple success or failure status is
required. ANSI C also specifies that using return from the function main is
equivalent to calling exit with status equal to the value returned. In many
non-ANSI implementations, however, the return value from main is discarded, so
it is best to use exit.
The only way that a process can terminate without explicitly calling exit
is by receiving a signal whose handler function is set to SIG_DFL (default
action). When this occurs, exit is called automatically with the number of the
received signal as the status argument.
Awaiting Process Termination
A process may await the termination of a child using the wait system call.
wait allows a process to synchronize its execution with the termination of a
child. When a process calls wait, the operating system first checks to see if
that process has any existing children. If it does not, wait returns a value
of -1 with errno set to ECHILD. If the process has a child which has
terminated (i.e., is a zombie), the PID of that child is returned by wait and
it is removed from the process table. If wait is called by a process with
children but none of them have terminated, the calling process suspends
execution until a signal is received. If a SIGCLD signal is received, the
process will wake up and wait will check the process table for zombie children
again.
The information returned in the integer pointed to by statusp depends upon
how the child terminated. If it terminated by calling exit, bits 0 to 6 of the
integer will be set to zero and bits 8 to 15 will contain the status argument
passed to exit. If the child terminated due to a signal, bits 0 to 6 will
contain the number of the signal and bits 8 to 15 will be 0. Some signals
cause a core image to be produced for debugging purposes, in which case bit 7
will be set. When using process tracing, a process may be stopped by a signal.
Here, bits 0 to 6 of the integer will all be set to one and bits 8 to 15 will
contain the number of the signal. For more information on tracing see section
11.1 of BACH86. The code fragment below outlines the use of wait. Some macros
are defined to make calls to wait more readable.
#define WT_MASK (0x7F)
#define WT_EXITED (0x00)
#define WT_STOPPED (0x7F)
#define WT_CORE (0x80)
#define WT_BITS (8)
int pid, status, sig;
pid = wait(&status);
if (pid == -1) {
if (errno == ECHILD) {
/* process has no existing children */
} else {
/* error */
}
} else {
/* pid == PID of the child */
switch (status & WT_MASK) {
case WT_EXITED: /* child terminated by calling exit */
status >>= WT_BITS; /* value passed to exit */
break;
case WT_STOPPED: /* child stopped due to a signal */
sig = status >> WT_BITS;
break;
default: /* child terminated due to a signal */
sig = status & WT_MASK;
if (status & WT_CORE) {
/* core image was produced */
}
break;
}
}
It is often desired for a process to wait for the termination of a
specific child, usually immediately after the call to fork that creates it.
This can be accomplished using the following construct.
int pid, status;
while (wait(&status) != pid)
;
A process may await the termination of all its children by setting SIGCLD
to be ignored before calling wait. wait will still remove zombie children from
the process table when SIGCLD is received, but it will not return until all
children have terminated. When there are no more unwaited-for children, wait
will return a value of -1 and with errno set to ECHILD.
int status;
signal(SIGCLD, SIG_IGN);
wait(&status);
if (errno != ECHILD) {
/* error */
}
zombie children may remain in the process table indefinitely. A parent
could create a child then enter a loop, never calling either wait or exit. The
process dispatcher calls wait regularly to prevent orphan zombies from
cluttering up the process table.
Process Groups
In addition to the process ID used to identify individual processes, each
process also has a process group ID (PGID) which is used to identify groups of
processes. The PGID is inherited by a child from its parent. Unlike the PID,
a process can change its PGID, but only by creating a new group. This is done
using the setpgrp system call.
int setpgrp();
setpgrp sets the PGID of the calling process to the same value as its PID,
returning the new PGID. Because the process which calls setpgrp is the first
member of the new group and only its descendants may belong to that group (by
inheriting its PGID), it is referred to as the process group leader.
A process can determine its PGID using the getpgrp system call.
int getpgrp();
getpgrp returns the PGID of the calling process. Because the PID of the group
leader is the same as the PGID, getpgrp also identifies the group leader.
Since only descendants of a process group leader can be members of that
process group, there is a direct correlation between process groups and the
process tree. Each process group leader is the root of a subtree which, after
the subtrees of descendant group leaders have been pruned, contains only
processes belonging to that group. If no members of the group have terminated
leaving children which have been adopted by the process dispatcher, this
subtree contains all the processes in that group.
A process can be associated with a terminal, which is called the control
terminal for that process. The control terminal is inherited from the parent
when a new process is created. A process is disassociated from its control
terminal when it calls setpgrp, becoming a process group leader (setpgrp does
not close the terminal). Also, only a process group leader can establish a
control terminal, becoming the control process for that terminal. This is done
automatically the first time it opens a terminal after calling setpgrp.
Neither opening subsequent terminals nor closing the control terminal will
affect this association. The standard input, output, and error are not
necessarily directed to the control terminal.
When a process group leader terminates, the PGIDs of all members of the
group are set to 0. If the group leader is assocated with a control terminal,
a hangup signal (SIGHUP) is sent to all members of the group, causing them to
terminate if the SIGHUP handler is set to SIG_DFL. This is how shell
background processes are terminated when a user logs out.
A process that is not associated with a control terminal is called a
daemon. The printer spooler is an example of a such a process. Daemons can be
identified from the ps command listing by a ? in the TTY column. Note that
running a process in the background from the shell using & does not make it a
daemon. Normally the shell waits for a child to terminate, but when a process
is run in the background it simply skips the wait and returns the prompt.
There is no call to setpgrp and so the control terminal is not disconnected.
The appinit Program
Multiuser applications execute as multiple processes, one for each
terminal from which the application may be accessed. Usually there are also
some daemon processes which perform administrative functions. One way to start
up such an application is to run the daemons manually, then go to each terminal
and run the appropriate interactive program for that location. On a large
system, however, there can be a large number of terminals located at great
distances from each other.
The appinit program is designed using the system calls discussed above to
automate the procedure for the startup of a multiuser application. It is a
process dispatcher similar to /etc/init but for use with application software.
Like /etc/init, appinit reads the process specifications from a control file.
Entries in this control file have the format tty command.
tty names the control terminal for the process to be created. If this
field does not begin with a /, the prefix "/dev/" is automatically added. A
daemon may be created by specifying a control terminal of /dev/null. command
is a command line specifying the program to be executed.
The source code for appinit is shown in Figure 3. There are two major
loops. The first is the process creation loop. Here fork is called to create
each process. Following the call to fork the new child then calls setpgrp to
become a process group leader and disconnect itself from the parent's control
terminal; since each child process becomes a process group leader, appinit may
be terminated without a SIGHUP signal being sent to terminate the dispatched
processes. The standard input, output, and error are then redirected to the
new control terminal, after which ioctl is used to set the parameters (device
I/O options; see ATT88b) for the new control terminal to the values saved from
the old one. execsp is then called to execute the command read from the
control file, allowing the use of the metacharacters \, ', ", and #.
In the second loop, appinit monitors the processes it has created. It
does this by calling wait repeatedly until all its children have terminated.
Details on the termination of each process is displayed.
When using appinit, the control terminals for the processes to be created
must be disabled. This is good for systems where security is important,
because if an application program terminates for some reason, all that will be
left is a dead terminal.
This version of appinit is a lean implementation. One useful enhancement
would be to allow processes to be terminated and new ones created on command.
Also, /etc/init will automatically respawn specified processes if they
terminate, and this would be a useful feature for appinit.
References
BACH86 Bach, M. The Design of the UNIX Operating System. Englewood Cliffs,
NJ: Prentice Hall, 1986.
BOUR83 Bourne, S. The UNIX System. Reading, MA: Addison-Wesley, 1983.
KERN84 Kernighan, B., and R. Pike. The UNIX Programming Environment.
Englewood Cliffs, NJ: Prentice Hall, 1984.
ATT88a AT&T. UNIX System V Programmer's Reference Manual. Englewood Cliffs,
NJ: Prentice Hall, 1988.
ATT88b AT&T. UNIX System V User's Reference Manual. Englewood Cliffs, NJ:
Prentice Hall, 1988.
Figure 1. fork and File Buffering
#include <stdio.h>
#include <stdlib.h>
int fork();
int main(int argc, char *argv[])
{
FILE *fp = NULL;
/* open file and write one line */
fp = fopen("outfile.txt", "w");
if (fp == NULL) {
perror("fopen");
exit(EXIT_FAILURE);
}
fputs("One line written to file.\n", fp);
/* create new process */
if (fork() == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
/* close file (buffers flushed automatically) */
fclose(fp);
exit(EXIT_SUCCESS);
}
Figure 2. execs Function
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "syscalls.h"
char *cmdtok(char *s);
/* execs: execute a file (string parameter) */
int execs(const char *cmdlin)
{
char *tcmdlin = NULL; /* tmp command line */
int argc = 0; /* argument count */
char **argv = NULL; /* argument vector */
char *p = NULL; /* gp char pointer */
/* make working copy of cmdlin */
tcmdlin = (char *)calloc((size_t)(strlen(cmdlin) + 1), sizeof(*tcmdlin));
strcpy(tcmdlin, cmdlin);
/* construct argv array from cmdlin */
argc = 0;
argv = NULL;
p = cmdtok(tcmdlin);
while (1) {
/* allocate memory for new argv element */
argv = (char **)realloc(argv, (size_t)((argc + 1) * sizeof(*argv)));
if (p == NULL) {
argv[argc] = NULL;
break;
}
argv[argc] = (char *)calloc((size_t)(strlen(p) + 1),
sizeof(*argv[argc]));
strcpy(argv[argc++], p);
p = cmdtok(NULL);
}
/* execute program */
execv(argv[0], argv);
/* free memory */
while (--argc >= 0) {
free(argv[argc]);
}
free(argv);
free(tcmdlin);
return -1;
}
Figure 3. appinit Program
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <termio.h>
#include "syscalls.h"
/* aps: report appinit process status */
void aps(pidc, pidv, ttyv, cmdlinv)
int pidc;
int pidv[];
char *ttyv[];
char *cmdlinv[];
{
int i = 0;
printf("TTY COMMAND PID\n");
for (i = 0; i < pidc; i++) {
printf("%-6s %-25s", ttyv[i] + 5, cmdlinv[i]);
if (pidv[i] != 0) {
printf(" %5d\n", pidv[i]);
} else {
printf("<terminated>\n");
}
}
return;
}
int main(int argc, char *argv[])
{
struct termio ttyparms; /* terminal parameters */
int pidc = 0; /* process count */
int *pidv = NULL; /* PID vector */
char **ttyv = NULL; /* tty device path name vector */
char **cmdlinv = NULL; /* command line vector */
int pid = 0; /* PID */
int status = 0; /* wait status */
int i = 0; /* gp loop index */
/* read control file, build ttyv and cmdlinv, set pidc, allocate pidv */
.
.
.
/* get control terminal parameter settings */
ioctl(0, TCGETA, &ttyparms);
/* create child processes */
for (i = 0; i < pidc; i++) {
pidv[i] = fork();
switch (pidv[i]) {
case -1: /* error */
perror("fork");
exit(EXIT_FAILURE);
break;
case 0: /* child process */
setpgrp(); /* make process group leader */
if (strcmp(ttyv[i], "/dev/null") != 0) {
/* connect new control terminal */
close(0); /* stdin */
if (open(ttyv[i], O_RDONLY) == -1) {
perror("opening stdin");
exit(EXIT_FAILURE);
}
close(1); /* stdout */
if (open(ttyv[i], O_WRONLY) == -1) {
perror("opening stdout");
exit(EXIT_FAILURE);
}
close(2); /* stderr */
if (open(ttyv[i], O_WRONLY) == -1) {
exit(EXIT_FAILURE);
}
/* set control terminal parameter settings */
ioctl(0, TCSETAF, &ttyparms);
}
/* execute program */
execsp(cmdlinv[i]);
perror("execsp");
exit(EXIT_FAILURE);
break;
default: /* parent process */
printf("Process %d running on %s.\n", pidv[i], ttyv[i]);
continue;
break;
}
}
/* monitor child processes */
for (;;) {
putchar('\n');
aps(pidc, pidv, ttyv, cmdlinv);
putchar('\n');
pid = wait(&status);
if (pid == -1) {
if (errno == ECHILD) {
printf("All child processes terminated.\n");
break;
}
perror("wait");
exit(EXIT_FAILURE);
}
for (i = 0; i < pidc; i++) {
if (pidv[i] == pid) {
pidv[i] = 0;
break;
}
}
if (i >= pidc) {
continue; /* unknown child */
}
printf("%s: ", ttyv[i]);
switch (status & WT_MASK) {
case WT_EXITED:
printf("Process %d terminated with exit(%d).\n",
pid, status >> WT_BITS);
break;
case WT_STOPPED:
printf("Process %d stopped due to signal %d.\n",
pid, status >> WT_BITS);
break;
default:
printf("Process %d terminated due to signal %d.",
pid, status & WT_MASK);
if (status & WT_CORE) {
printf(" A core image was produced.");
}
printf("\n");
break;
}
}
exit(EXIT_SUCCESS);
}